﻿namespace LuckyAssetManagerContrib.FileCache
{
	using System;
	using System.Collections.Generic;
	using System.IO;
	using System.Linq;
	using System.Threading;
	using System.Web;
	using System.Xml.Serialization;

	using Configuration;
	using Resources;

	/// <summary>
	/// File cache manager
	/// </summary>
	public sealed class AssetFileCacheManager : IDisposable
	{
		/// <summary>
		/// Configuration settings of file cache manager
		/// </summary>
		private readonly AssetFileCacheManagerElement _config;

		/// <summary>
		/// Registry of file cache
		/// </summary>
		private AssetFileCacheRegistry _items;

		/// <summary>
		/// Synchronizer of file cache registry
		/// </summary>
		private readonly object _itemsSynchronizer = new object();

		/// <summary>
		/// Absolute path to directory that contains file cache
		/// </summary>
		private readonly string _assetFileCacheDirectoryPath;

		/// <summary>
		/// Path to XML-file, which stores file cache registry
		/// </summary>
		private readonly string _assetFileCacheRegistryFilePath;

		/// <summary>
		/// Timer, which starts removing expired file cache items
		/// </summary>
		private Timer _removeTimer;

		/// <summary>
		/// Timer, which starts saving file cache registry
		/// </summary>
		private Timer _saveTimer;

		/// <summary>
		/// Flag that object is destroyed
		/// </summary>
		private bool _disposed;


		public AssetFileCacheManager() : this(
			AssetManagerContext.ContribConfiguration.AssetFileCacheManager)
		{ }

		public AssetFileCacheManager(AssetFileCacheManagerElement config)
		{
			_config = config;

			// Calculate absolute paths to file cache directory and registry
			HttpServerUtility server = HttpContext.Current.Server;
			_assetFileCacheDirectoryPath = server.MapPath(Constants.AssetFileCacheDirectoryPath);
			_assetFileCacheRegistryFilePath = Path.Combine(_assetFileCacheDirectoryPath, 
				Constants.AssetFileCacheRegistryFileName);

			// Load file cache registry from XML-file
			Load();

			// Initialize timer, which starts removing expired file cache items
			const int removeTimerIntervalInMilliseconds = 60 * 1000;
			var removeTimerCallback = new TimerCallback(RemoveTimerCallback);
			_removeTimer = new Timer(removeTimerCallback, this, 0, removeTimerIntervalInMilliseconds);

			// Initialize timer, which starts saving file cache registry
			int saveTimerIntervalInMilliseconds = _config.FileCacheRegistrySaveIntervalInMinutes * 60 * 1000;
			var saveTimerCallback = new TimerCallback(SaveTimerCallback);
			_saveTimer = new Timer(saveTimerCallback, this, saveTimerIntervalInMilliseconds, 
				saveTimerIntervalInMilliseconds);
		}

		~AssetFileCacheManager()
		{
			Dispose(false);
		}


		/// <summary>
		/// Load file cache registry from XML-file
		/// </summary>
		public void Load()
		{
			lock (_itemsSynchronizer)
			{
				var items = new AssetFileCacheRegistry();

				if (File.Exists(_assetFileCacheRegistryFilePath))
				{
					using (var file = new StreamReader(_assetFileCacheRegistryFilePath))
					{
						var serializer = new XmlSerializer(typeof (AssetFileCacheRegistry));
						try
						{
							items = (AssetFileCacheRegistry) serializer.Deserialize(file);
						}
						catch
						{
							throw new InvalidOperationException(Strings.FileCache_RegistryDeserializationFailed);
						}
					}
				}

				_items = items;
			}
		}

		/// <summary>
		/// Returns file cache item by key
		/// </summary>
		/// <param name="key">Cache item key</param>
		/// <returns>Cache item</returns>
		public AssetFileCacheItem GetItem(string key)
		{
			lock (_itemsSynchronizer)
			{
				AssetFileCacheItem item = null;

				if (_items.ContainsKey(key))
				{
					item = _items[key];
					if (item != null)
					{
						item.LastAccessTime = DateTime.Now;
					}
				}

				return item;
			}
		}

		/// <summary>
		/// Checks existance of file cache item by key
		/// </summary>
		/// <param name="key">Cache item key</param>
		/// <returns>Result of checking (true – exist; false – not exist)</returns>
		public bool ContainsItem(string key)
		{
			var item = GetItem(key);

			return (item != null);
		}

		/// <summary>
		/// Inserts new item to file cache
		/// </summary>
		/// <param name="key">Cache item key</param>
		/// <param name="content">Text content of processed asset</param>
		/// <param name="associatedFilePaths">List of file paths associated 
		/// with processed asset</param>
		/// <param name="lastModifyTime">Date and time of last modification 
		/// files associated with processed asset</param>
		public void InsertItem(string key, string content, 
			string[] associatedFilePaths, DateTime lastModifyTime)
		{
			lock (_itemsSynchronizer)
			{
				DateTime currentTime = DateTime.Now;

				var item = new AssetFileCacheItem
				{
				    Key = key,
				    AssociatedFilePaths = associatedFilePaths,
				    LastModifyTime = lastModifyTime,
					CreationTime = currentTime,
					LastAccessTime = currentTime,
				    TempFileName = String.Format("{0}.txt", key)
				};
				item.WriteTempFileContent(content);

				_items[key] = item;
			}
		}

		/// <summary>
		/// Removes file cache item
		/// </summary>
		/// <param name="key">Cache item key</param>
		public void RemoveItem(string key)
		{
			lock (_itemsSynchronizer)
			{
				if (_items.ContainsKey(key))
				{
					AssetFileCacheItem item = _items[key];
					item.RemoveTempFile();

					_items.Remove(key);
				}
			}
		}

		/// <summary>
		/// Updates date and time of last access to 
		/// specified file cache item
		/// </summary>
		/// <param name="key">Cache item key</param>
		public void UpdateItemLastAccessTime(string key)
		{
			GetItem(key);
		}

		/// <summary>
		/// Removes expired items from file cache
		/// </summary>
		public void RemoveExpiredItems()
		{
			lock (_itemsSynchronizer)
			{
				DateTime expiredItemsTime = DateTime.Now.AddMinutes(-1 * _config.FileCacheDurationInMinutes);

				List<AssetFileCacheItem> expiredItems;
				if (_config.UseFileCacheSlidingExpiration)
				{
					expiredItems = _items.Values
						.Where(i => i.LastAccessTime <= expiredItemsTime)
						.ToList()
						;
				}
				else
				{
					expiredItems = _items.Values
						.Where(i => i.CreationTime <= expiredItemsTime)
						.ToList()
						;
				}

				foreach (var item in expiredItems)
				{
					item.RemoveTempFile();
					_items.Remove(item.Key);
				}
			}
		}

		/// <summary>
		/// Saves file cache registry to XML-file
		/// </summary>
		public void Save()
		{
			if (!Directory.Exists(_assetFileCacheDirectoryPath))
			{
				Directory.CreateDirectory(_assetFileCacheDirectoryPath);
			}

			lock (_itemsSynchronizer)
			{
				if (_items != null)
				{
					using (var file = new StreamWriter(_assetFileCacheRegistryFilePath))
					{
						var serializer = new XmlSerializer(typeof (AssetFileCacheRegistry));
						serializer.Serialize(file, _items);
					}
				}
			}
		}

		/// <summary>
		/// Callback function for timer, which starts removing 
		/// expired file cache items
		/// </summary>
		/// <param name="state">Object state</param>
		private void RemoveTimerCallback(object state)
		{
			RemoveExpiredItems();
		}

		/// <summary>
		/// Callback function for timer, which starts saving 
		/// file cache registry
		/// </summary>
		/// <param name="state">Object state</param>
		private void SaveTimerCallback(object state)
		{
			Save();
		}

		/// <summary>
		/// Destroys object
		/// </summary>
		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		/// <summary>
		/// Destroys object
		/// </summary>
		/// <param name="disposing">Flag, allowing destruction of 
		/// managed objects contained in fields of class</param>
		protected void Dispose(bool disposing)
		{
			if (!_disposed)
			{
				_disposed = true;

				if (_saveTimer != null)
				{
					_saveTimer.Dispose();
					_saveTimer = null;
				}

				if (_removeTimer != null)
				{
					_removeTimer.Dispose();
					_removeTimer = null;
				}

				Save();

				if (_items != null)
				{
					_items.Clear();
					_items = null;
				}
			}
		}
	}
}
